If your homelab has more than a couple of drives, you already know the problem: smartctl works fine, but its output is a wall of raw numbers, smartd emails are cryptic, and nothing tracks how an attribute is trending. That last bit matters most — drives rarely die out of nowhere, they degrade first. This is the setup notes from getting Scrutiny running on Proxmox: a self-hosted dashboard with drive health, historical graphs, and alerts before something actually fails.
I'm using the official Hub-Spoke layout, which is the architecture the Scrutiny maintainers themselves recommend: the web UI and database live safely inside an unprivileged LXC, while a small collector binary runs directly on the Proxmox host — the only place with proper low-level access to the physical disks.
Why Not Just Cram Everything Into One LXC
The tempting shortcut is a single LXC with the disks passed through and everything running inside it. I'd avoid that, for a few reasons:
- An unprivileged LXC (the sane default) can't easily read SMART data off passed-through block devices — they show up owned by
nobody:nogroupandsmartctljust returnsOperation not permitted. This is a long-standing complaint on the Proxmox forums. - NVMe specifically wants the
SYS_ADMINcapability, which isn't something worth handing to an unprivileged container. - A privileged LXC would technically work, but then you're running a web-facing service with near-root access to the host. No thanks.
Hub-Spoke sidesteps all of it:
- Hub — Scrutiny's web UI + InfluxDB, inside an unprivileged LXC running Docker. No disk passthrough needed at all.
- Spoke (collector) — a tiny binary on the Proxmox host itself, where
/dev/sda,/dev/nvme0, etc. are natively accessible. It reads the SMART data and posts it to the Hub over HTTP.
What You Need
- A Proxmox VE 8.x/9.x host with some drives attached — SATA or NVMe, doesn't matter.
- Shell access to the host.
- A network where the LXC and the host can reach each other, which is the default anyway.
- About fifteen minutes.
Step 1 — Create the Docker LXC (the Hub)
The Hub runs the web UI and InfluxDB. The quickest way to get a Debian LXC with Docker pre-installed is the Community Scripts helper. From the Proxmox host shell:
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/docker.sh)"
Note: community scripts run as root on your host. It's worth a quick skim before running one, even a well-known one like this.
During the prompts: pick Advanced mode if you want to tune things, give it 2 cores / 1–2GB RAM / 8–10GB disk (plenty for Scrutiny + InfluxDB), leave it unprivileged, and set a static IP if you can — the collector will need to point at it later.
Enter the container once it's up:
pct enter <CTID>
And sanity-check Docker:
docker --version
docker compose version
Step 2 — Deploy Scrutiny Web + InfluxDB via Docker Compose
Inside the LXC, set up the working directory:
mkdir -p /opt/scrutiny && cd /opt/scrutiny
2a. The config file has to exist first
This tripped me up initially — scrutiny-web needs a config file before its first launch, or it panics trying to build the InfluxDB connection string from nothing:
mkdir -p /opt/scrutiny/config
cat > /opt/scrutiny/config/scrutiny.yaml << 'EOF'
version: 1
web:
listen:
port: 8080
host: 0.0.0.0
database:
location: /opt/scrutiny/config/scrutiny.db
src:
frontend:
path: /opt/scrutiny/web
influxdb:
host: influxdb
port: 8086
EOF
2b. docker-compose.yml
This mirrors the project's official hub-only compose example — just the web UI and InfluxDB. The collector doesn't belong in here at all, it lives on the host.
cat > /opt/scrutiny/docker-compose.yml << 'EOF'
services:
influxdb:
image: influxdb:2.8
container_name: scrutiny-influxdb
restart: unless-stopped
ports:
- "8086:8086"
volumes:
- ./influxdb:/var/lib/influxdb2
networks:
- scrutiny-net
healthcheck:
test: ["CMD", "influx", "ping"]
interval: 10s
timeout: 5s
retries: 10
scrutiny-web:
image: ghcr.io/analogj/scrutiny:v0.8.6-web
container_name: scrutiny-web
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- ./config:/opt/scrutiny/config
depends_on:
influxdb:
condition: service_healthy
networks:
- scrutiny-net
networks:
scrutiny-net:
driver: bridge
EOF
A few details worth flagging, since none of them are obvious from the error messages alone:
- Pin the version to v0.8.6. Don't use
master-weborlatest— the web image and the collector binary must match exactly.v0.9.0-webintroduced a breaking change where/api/devices/registersilently returns{"success":false}, so drives never show up. - No environment variables for InfluxDB. Scrutiny reads that connection from
scrutiny.yamlonly — variables likeSCRUTINY_WEB_INFLUXDB_HOSTlook right but are quietly ignored, and you get a startup panic instead. - The healthcheck matters. Without
condition: service_healthy,scrutiny-webstarts before InfluxDB is actually ready and crashes.
2c. Start it
docker compose up -d
Give InfluxDB about 20 seconds to pass its healthcheck, then check the logs:
docker compose logs scrutiny-web
You want to see Successfully connected to scrutiny sqlite db and Database migration completed successfully, with no panics. At that point http://<LXC_IP>:8080 should load — empty, since no collector has reported in yet.
Step 3 — Install the Collector on the Proxmox Host
Back on the Proxmox host (not the LXC), install smartmontools if it isn't already there:
apt install smartmontools -y
Then pull the collector binary — again, v0.8.6 specifically, to match the web image:
mkdir -p /opt/scrutiny/bin /opt/scrutiny/config
wget -O /opt/scrutiny/bin/scrutiny-collector-metrics \
https://github.com/AnalogJ/scrutiny/releases/download/v0.8.6/scrutiny-collector-metrics-linux-amd64
chmod +x /opt/scrutiny/bin/scrutiny-collector-metrics
Version pinning isn't optional here. Grabbing
releases/latestwill hand you v0.9.0, and the dashboard will silently stay empty even though the collector runs without errors.
Confirm it:
/opt/scrutiny/bin/scrutiny-collector-metrics --version
Expected: linux.amd64-0.8.6.
3a. collector.yaml — needed for cron to actually work
This is the one that cost me the most time. When cron runs the collector, it uses a stripped-down PATH that doesn't include /usr/sbin, so it can't find smartctl and fails with DependencyMissingError: "smartctl binary is missing" — even though running it manually works perfectly. The fix is pointing at the full binary path explicitly:
cat > /opt/scrutiny/config/collector.yaml << 'EOF'
version: 1
commands:
metrics_smartctl_bin: /usr/sbin/smartctl
host:
id: "proxmox-main"
EOF
host.id is just a label — useful once you're feeding more than one host into the same dashboard, which I do below.
3b. Test run
/opt/scrutiny/bin/scrutiny-collector-metrics run \
--api-endpoint "http://<LXC_IP>:8080"
You should see each drive get detected and posted one by one. Refresh the dashboard — the drives should now be listed.
Step 4 — Schedule It with Cron
Edit the root crontab. The collector needs root to read SMART data. On any host where you log in as a non-root user, use
sudo crontab -e— adding the job to a regular user's crontab just fails silently on permissions.
On the Proxmox host (already root):
crontab -e
Add a line for every 30 minutes:
*/30 * * * * /opt/scrutiny/bin/scrutiny-collector-metrics run --api-endpoint "http://<LXC_IP>:8080" >/dev/null 2>&1
On frequency: 30 minutes is a reasonable default — enough resolution for temperature trends without bloating InfluxDB. Go to 15 minutes if you want finer granularity, or hourly if you'd rather keep the database small.
Double-check it saved, and that cron itself is actually running:
crontab -l
systemctl status cron
Troubleshooting Notes
A drive is missing or shows "Unknown"
Usually the collector couldn't auto-detect the device type — common with USB enclosures or SAS/RAID controllers. Give it an explicit hint in collector.yaml:
version: 1
host:
id: "proxmox-main"
devices:
- device: /dev/sda
type: 'sat'
- device: /dev/sdb
type: 'sat'
- device: /dev/nvme0
type: 'nvme'
Then point the cron entry at that config with --config /opt/scrutiny/config/collector.yaml. Use smartctl --scan to list your actual devices.
NVMe shows no temperature or wear data
This comes down to the smartmontools version on the host — NVMe reports a different attribute schema than SATA, and anything older than 7.0 handles it poorly. Check with smartctl --version.
"Connection refused" from the collector
Almost always a firewall blocking port 8080 between the host and the LXC. Test from the host with curl -v http://<LXC_IP>:8080 and check both the LXC and Proxmox firewall rules if it times out.
Dashboard stays empty even though the collector runs fine
Nine times out of ten this is a version mismatch between the web image and the collector binary. Check both:
# On the LXC
docker compose logs scrutiny-web | grep "linux\|dev-"
# On the Proxmox host
/opt/scrutiny/bin/scrutiny-collector-metrics --version
If they don't match, re-pull the v0.8.6 collector and reset the web container:
docker compose down
sed -i 's|scrutiny:.*-web|scrutiny:v0.8.6-web|' docker-compose.yml
rm -f ./config/scrutiny.db && rm -rf ./influxdb
docker compose up -d
Feeding in Other Hosts
The nice part of Hub-Spoke is that one Hub can take collectors from several machines, not just the Proxmox host it's paired with. Any other Debian-based box gets the same collector binary and steps. The one thing to remember: give each host a unique host.id in its own collector.yaml, or Scrutiny can't tell the drives apart on the dashboard.
Wrapping Up
End state: an unprivileged LXC running the dashboard and InfluxDB, a native collector on the Proxmox host with proper access to the drives, historical trend data. The CPU overhead is negligible, so there's not much reason not to run this once it's set up.